package org.syntaxlisp.core;

import java.util.ArrayList;
import java.util.List;

import static org.syntaxlisp.core.utils.CommonUtils.*;

/**
 * @author HuangYijun
 * @data 2022-12-02 17:33
 **/
public class LispListParser {

    public static final char ESCAPE = '\\';

    public static final char LEFT_PARENTHESIS = '(';

    public static final char RIGHT_PARENTHESIS = ')';

    public static final char SINGLE_QUOTE = '\'';

    public static List<Element> parse (CharSequence charStream) {
        ArrayList<Element> elementList = new ArrayList<>();
        int i = 0;
        do {
            ParseRes parseRes = parse(charStream, i);
            elementList.add(parseRes.element);
            i = parseRes.endPos;
        } while (i < charStream.length());
        return elementList;
    }

    public static ParseRes parse (CharSequence charStream, int start) {
        int i = skipInvisibleAsciiChars(charStream, start);
        if (i >= charStream.length()) { return new ParseRes (null, i); }
        char c = charStream.charAt(i);
        if (c == LEFT_PARENTHESIS) {
            Element list = new Element();
            list.setElementType(ElementType.LIST);
            list.setSubItems(new ArrayList<>());
            i += 1;
            while ((i = skipInvisibleAsciiChars(charStream, i)) < charStream.length()
                    && charStream.charAt(i) != RIGHT_PARENTHESIS) {
                ParseRes parseRes = parse(charStream, i);
                list.getSubItems().add(parseRes.element);
                i = parseRes.endPos;
            }
            checkOrThrow(i < charStream.length(), "Unexpected end while parsing list");
            return new ParseRes(list, i + 1);
        } else if (c == SINGLE_QUOTE) {
            Element string = new Element();
            string.setElementType(ElementType.STRING);
            StringBuilder value = new StringBuilder();
            i += 1;
            while (i < charStream.length()
                    && charStream.charAt(i) != SINGLE_QUOTE) {
                if (charStream.charAt(i) == ESCAPE) {
                    checkOrThrow(charStream.length() > (i + 1), "Unexpected end while parsing string");
                    checkOrThrow(charStream.charAt(i + 1) == SINGLE_QUOTE
                                            || charStream.charAt(i + 1) == ESCAPE,
                                          "Expected escape ' at pos " + (i + 1));
                    value.append(charStream.charAt(i + 1));
                    i += 2;
                } else {
                    value.append(charStream.charAt(i));
                    i += 1;
                }
            }
            checkOrThrow(i < charStream.length(), "Unexpected end while parsing string");
            string.setValue(value.toString());
            return new ParseRes(string, i + 1);
        } else if (isTokenCharacter(c)) {
            Element token = new Element();
            StringBuilder value = new StringBuilder();
            token.setElementType(ElementType.TOKEN);
            while (i < charStream.length()
                    && isTokenCharacter(charStream.charAt(i))) {
                value.append(charStream.charAt(i));
                i += 1;
            }
            token.setValue(value.toString());
            return new ParseRes(token, i);
        } else {
            throw new ParseException("Unexpected character " + c);
        }
    }

    private static boolean isTokenCharacter (char ch) {
        return !isReservedCharacter(ch) && isVisibleChar(ch);
    }

    private static boolean isReservedCharacter (char ch) {
        return ch == LEFT_PARENTHESIS || ch == RIGHT_PARENTHESIS || ch == SINGLE_QUOTE || ch == ESCAPE;
    }

    private static class ParseRes {

        public ParseRes(Element element, int endPos) {
            this.element = element;
            this.endPos = endPos;
        }

        final Element element;

        final int endPos;
    }

    private static int skipInvisibleAsciiChars(CharSequence charSequence, int start) {
        int i = start;
        while (i < charSequence.length()
               && !isVisibleChar(charSequence.charAt(i))) {
            i += 1;
        }
        return i;
    }

    public static boolean isVisibleChar (char ch) {
        return ch > 32 && ch != 127;
    }
}